﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using FMOD;

using Rockfan.Framework;
using Rockfan.Framework.Audio;
using Rockfan.Framework.Entities;
using Rockfan.Framework.Entities.Interfaces;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Rockfan.Framework.Audio
{
    public sealed class SoundFX : ISound
    {
        #region Properties

        /// <summary>
        /// Gets a string value representing the name of this object
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// Gets a value indicating an amount in seconds when this object was created
        /// </summary>
        public float TimeCreated { get; private set; }

        /// <summary>
        /// Gets the total amount of 'Playing' instances
        /// </summary>
        public int Instances
        {
            get { return Channels.Count; }
        }

        /// <summary>
        /// Gets a value indicating whether there are any 'Instances' currently playing
        /// </summary>
        public bool IsPlaying
        {
            get { return Instances > 0; }
        }

        /// <summary>
        /// Gets or Sets a value indicating whether this object should play in loop mode
        /// </summary>
        public bool IsLooped
        {
            get { return _IsLooped; }
            set
            {
                var oldValue = IsLooped;
                _IsLooped = value;

                if (IsLooped != oldValue)
                    SetLooped(IsLooped);
            }
        }

        /// <summary>
        /// Gets or Sets a value indicating the volume at which this object is played
        /// </summary>
        public float Volume
        {
            get { return _Volume; }
            set
            {
                var oldValue = Volume;
                _Volume = MathHelper.Clamp(value, 0.0f, 100.0f);

                if (Volume != oldValue)
                    SetVolume(Volume);
            }
        }

        /// <summary>
        /// Gets or Sets a value indicating a percetage of the base playspeed, that this object should be played
        /// </summary>
        public float Playspeed
        {
            get { return _Playspeed; }
            set
            {
                var oldValue = Playspeed;
                _Playspeed = MathHelper.Clamp(value, 50.0f, 400.0f);

                if (Playspeed != oldValue)
                    SetPlayspeed(Playspeed);
            }
        }

        /// <summary>
        /// Gets a value indicating the total length in seconds of this object
        /// </summary>
        public float Playlength
        {
            get { return _Playlength; }
        }

        // We dont' care about the play position, since there can be multiple instances playing at the same time
        float ISound.Playposition { get { return _Playposition; } }

        // We ALWAYS need to be updated, to remove channels, so the system can dispose of them
        bool IUpdateableEntity.EnableUpdates { get { return true; } set { } }

        #endregion

        #region Methods

        #region Constructor

        /// <summary>
        /// Constructs a new instance of this object
        /// </summary>
        /// <param name="fileLocation">a string value representing the location on disk to load this sound data</param>
        /// <param name="maxInstances">an integer value indicating the maximum amount of simultaneous sounds this object can play</param>
        public SoundFX(string fileLocation, int maxInstances)
        {
            Channels = new List<Channel>();

            if (File.Exists(fileLocation))
            {
                this.FileLocation = fileLocation;
                this.MaxInstances = maxInstances;
                
                Sound = AudioEngine.CreateSound(fileLocation, false);
                LastError = AudioEngine.GetLastError();

                if (LastError == RESULT.OK)
                {
                    var playLength = (uint)0;
                    Sound.getLength(ref playLength, TIMEUNIT.MS);
                    LastError = AudioEngine.GetLastError();

                    _Playlength = (float)(playLength / 1000.0f);
                }

                _Volume = 100.0f;
                _Playspeed = 100.0f;
                _Playposition = 0.0f;
                _IsLooped = false;

                Name = Path.GetFileNameWithoutExtension(fileLocation);
                TimeCreated = EngineServices.TotalGameTime;
            }
            else LastError = RESULT.ERR_FILE_NOTFOUND;

            AudioEngine.AddSound(this);
        }

        #endregion

        /// <summary>
        /// Plays a new instance of this object
        /// </summary>
        public void PlayInstance()
        {
            CheckLastError();

            Channel channel = default(Channel);
            AudioEngine.PlaySound(Sound, ref channel);

            if (AudioEngine.GetLastError() != RESULT.OK)
                throw new AudioException(Error.String(AudioEngine.GetLastError()));

            if (BaseFrequency == 0 || BaseVolume == 0)
            {
                channel.getFrequency(ref BaseFrequency);
                channel.getVolume(ref BaseVolume);
            }

            channel.setPosition(0, TIMEUNIT.MS);
            channel.setVolume(BaseVolume * (Volume / 100.0f));
            channel.setFrequency(BaseFrequency * (Playspeed / 100.0f));
            channel.setPaused(false);

            Channels.Add(channel);

            if (Instances > MaxInstances)
                (this as IUpdateableEntity).Update(0.0f);
        }

        /// <summary>
        /// Stops all playing instances of this object
        /// </summary>
        public void StopAll()
        {
            if (Instances > 0)
            {
                foreach (var channel in Channels)
                    channel.stop();

                Channels.Clear();
            }
        }

        /// <summary>
        /// Destroys all instances of this object, and disposes of managed objects
        /// </summary>
        public void Destroy()
        {
            StopAll();
            
            if (Sound != null)
                Sound.release();

            _IsLooped = false;
            _Volume = 0.0f;
            _Playspeed = 0.0f;
            _Playlength = 0.0f;
            _Playposition = 0.0f;
            BaseVolume = 0;
            BaseFrequency = 0;
            FileLocation = string.Empty;
            Sound = null;
            LastError = RESULT.OK;
            Channels.Clear();
            Channels = null;
            MaxInstances = 0;

            AudioEngine.Remove(this);
            GC.SuppressFinalize(this);            
        }

        // Check the last Error from the AudioEngine
        private void CheckLastError()
        {
            if (LastError != RESULT.OK)
                throw new AudioException(Error.String(LastError));
        }

        // Set the Loop mode on all running instances
        private void SetLooped(bool isLooped)
        {
            var mode = default(MODE);
            var loopMode = isLooped ? MODE.LOOP_NORMAL : MODE.LOOP_OFF;

            if (Instances > 0)
            {
                foreach (var channel in Channels)
                {   
                    channel.getMode(ref mode);

                    RemoveLoopMode(ref mode);                    
                    channel.setMode(mode | loopMode);
                }
            }

            Sound.getMode(ref mode);
            RemoveLoopMode(ref mode);
            Sound.setMode(mode | loopMode);
        }

        // Set the Volume for all playing instances
        private void SetVolume(float value)
        {
            if (Instances > 0)
            {
                var trueVolume = value / 100.0f;

                foreach (var channel in Channels)
                    channel.setVolume(BaseVolume * trueVolume);
            }
        }

        /// Set the playback speed for all playing instances
        private void SetPlayspeed(float value)
        {
            if (Instances > 0)
            {
                var truePlayspeed = BaseFrequency * (value / 100.0f);

                foreach (var channel in Channels)
                    channel.setFrequency(truePlayspeed);
            }            
        }

        // Removes the Loop mode from the current mode of all playing instances
        private void RemoveLoopMode(ref MODE mode)
        {
            mode &= mode ^ MODE.LOOP_NORMAL;
            mode &= mode ^ MODE.LOOP_OFF;
        }

        // Updates any useful information needed by this object
        void IUpdateableEntity.Update(float delta)
        {
            CheckLastError();

            if (Instances > MaxInstances)
            {
                var channel = Channels[0];

                channel.stop();
                Channels.Remove(channel);
            }

            for (int i = Channels.Count - 1; i >= 0; i--)
            {
                var channel = Channels[i];
                var playPosition = (uint)0;

                channel.getPosition(ref playPosition, TIMEUNIT.MS);

                if (playPosition >= (uint)(_Playlength * 1000.0f))
                    Channels.RemoveAt(i);
            }
        }

        #region Empty interface implementations

        // Empty Methods, no need for these

        void ISound.Play() { }
        void ISound.Pause() { }
        void ISound.Stop() { }

        #endregion

        #endregion

        private int MaxInstances;

        private bool _IsLooped;
        private float _Volume;
        private float _Playspeed;
        private float _Playlength;
        private float _Playposition;

        private float BaseVolume;
        private float BaseFrequency;

        private string FileLocation;

        private Sound Sound;
        private RESULT LastError;
        private List<Channel> Channels;
    }
}
